package ch.zhaw.cctd.domain.map;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.zhaw.cctd.domain.GraphicalElement;
import ch.zhaw.cctd.domain.Movable;
import ch.zhaw.cctd.domain.Shootable;
import ch.zhaw.cctd.domain.Shooter;
import ch.zhaw.cctd.domain.creep.Creep;
import ch.zhaw.cctd.domain.match.MatchServer;
import ch.zhaw.cctd.domain.player.ConstructionZone;
import ch.zhaw.cctd.domain.player.Player;
import ch.zhaw.cctd.domain.player.RessourceException;
import ch.zhaw.cctd.domain.player.SpawnPoint;
import ch.zhaw.cctd.domain.prototype.CreepPrototype;
import ch.zhaw.cctd.domain.prototype.MapElementPrototype;
import ch.zhaw.cctd.domain.prototype.MapPrototype;
import ch.zhaw.cctd.domain.prototype.MapTexturePrototype;
import ch.zhaw.cctd.domain.prototype.SpawnPointPrototype;
import ch.zhaw.cctd.domain.prototype.TowerPrototype;
import ch.zhaw.cctd.domain.prototype.WayPointPrototype;
import ch.zhaw.cctd.domain.shot.Shot;
import ch.zhaw.cctd.domain.tower.Tower;
import ch.zhaw.cctd.domain.tower.TowerBuildException;

/**
 * Representiert das Spiel Feld. Unterhält eine Liste aller WayPoints,
 * SpawnPoints, MapElements und Creeps. Ist zuständig für das Creep spawnen auf
 * der Map und das bauen der Towers.
 * 
 * @author Rolf Koch
 * @version 1.0
 * 
 */
public class Map {

	/**
	 * Logger
	 */
	private static final Logger logger = LoggerFactory.getLogger(Map.class);

	// Match Server to have access to the Players
	private MatchServer match;

	// Creep Waypoints
	private List<WayPoint> wayPointList;

	// Creep Spawnpoints (Owned by Players)
	private List<SpawnPoint> spawnPointList;

	// Static Map Elements
	private List<MapElement> mapElementList;

	// Creep List
	private List<Creep> creepList;
	// Shot List
	private List<Shot> shotList;
	// Tower List
	private List<Tower> towerList;

	// Initial Ressources for each Player
	private int initialResources;
	// Map X Size
	private int mapXsize;
	// Map Y Size
	private int mapYsize;
	// Map Name
	private String name;
	// Map Max Creep (If maxCreep*playercount exists --> Lose game)
	private int maxCreep;

	/**
	 * Default Constructor
	 * 
	 * @param match
	 */
	public Map(MatchServer match) {
		this.match = match;

		// Movers
		creepList = Collections.synchronizedList(new ArrayList<Creep>());
		shotList = Collections.synchronizedList(new ArrayList<Shot>());

		// Shooter
		towerList = Collections.synchronizedList(new ArrayList<Tower>());

		// Waypoints
		spawnPointList = Collections.synchronizedList(new ArrayList<SpawnPoint>());
		wayPointList = Collections.synchronizedList(new ArrayList<WayPoint>());

		// Graphical Elements
		mapElementList = Collections.synchronizedList(new ArrayList<MapElement>());
	}

	/**
	 * Load the Map Properties from a MapPrototype
	 * 
	 * @param mapPrototype
	 */
	public void loadMapProperties(MapPrototype mapPrototype) {
		// Map Properties
		this.initialResources = mapPrototype.getInitialResources();
		this.setSize(mapPrototype.getSize(), mapPrototype.getSize());
		this.setName(mapPrototype.getName());
		this.maxCreep = mapPrototype.getMaxCreep();

		// Pathing Graph Building:
		List<SpawnPointPrototype> spawnPointPrototypes = mapPrototype.getSpawnPointPrototypes();
		logger.trace("XXXX  {} ",spawnPointPrototypes.size() );
		// Greate Graph Nodes
		for (SpawnPointPrototype s : spawnPointPrototypes) {
			SpawnPoint sp;
			logger.trace("XX create Spawnpoint {}", s.getPlayerId());
			try {
				Player owner = match.getPlayer(s.getPlayerId());
				if (owner != null) {
					sp = new SpawnPoint(s.getId(), owner, new Point(s.getX(), s.getY()), s.getNextWayPointId());

					spawnPointList.add(sp);
					// Set SpawnPoint

					owner.addSpawnPoint(sp);
					// Set Start Money
					owner.receiveRessource(initialResources);
				}
			} catch (RemoteException e) {
				logger.error("could not set the initial Ressources", e);
			}
		}
		List<WayPointPrototype> wayPointsPrototypes = mapPrototype.getWayPointsPrototypes();

		for (WayPointPrototype w : wayPointsPrototypes) {
			wayPointList.add(new WayPoint(w.getId(), new Point(w.getX(), w.getY()), w.getNextWayPointId()));
		}

		// Create Graph
		// Find targetWayPoints for all SpawnPoints
		for (SpawnPoint s : spawnPointList) {
			
			for (int i = 0; i < wayPointList.size(); i++) {
				WayPoint currentWayPoint = wayPointList.get(i);
				if (s.getNextWayPointId().equals(currentWayPoint.getId())) {
					logger.trace("GraphBuilder Spawnpoint: {} --> {}", s.getId(), currentWayPoint.getId());
					s.setTargetWayPoint(currentWayPoint);
					break;
				}
			}
			// Verify SpawnPoint NextPoint
			s.getTargetPoint();
		}

		// Find targetWayPoints for all Waypoints
		for (WayPoint w : wayPointList) {
			for (int i = 0; i < wayPointList.size(); i++) {
				WayPoint currentWayPoint = wayPointList.get(i);
				if (w.getNextWayPointId().equals(currentWayPoint.getId())) {
					logger.trace("GraphBuilder Waypoint {} --> {}", w.getId(), currentWayPoint.getId());
					w.setTargetWayPoint(currentWayPoint);
					break;
				}
			}
			// Verify WayPoint NextPoint
			w.getTargetPoint();
		}

		// Map Elements Building
		List<MapElementPrototype> mapElementPrototypes = mapPrototype.getMapElementPrototypes();
		List<MapTexturePrototype> mapTexturePrototypes = mapPrototype.getMapTexturePrototypes();

		for (MapElementPrototype w : mapElementPrototypes) {
			String texture = "";
			for (MapTexturePrototype t : mapTexturePrototypes) {
				if (t.getId().equals(w.getTextureId())) {
					texture = t.getTexturePath();
				}
			}
			if (w.isConstructionZone()) {
				try {
					Player owner = match.getPlayer(w.getPlayerid());
					if (owner != null) {

						ConstructionZone mcz = new ConstructionZone(w, texture);
						mapElementList.add(mcz);
						owner.addMapConstructionZone(mcz);
						logger.trace("MapElementBuilder: Constructionzone: player {}", w.getPlayerid());
					} else {
						logger.trace("MapElementBuilder: Constructionzone: Player not available");
					}
				} catch (RemoteException e) {
					logger.error("Network Error: Could not add the Constructionzone to the Player", e);
				}
			} else {
				logger.trace("MapElementBuilder: Static Element");
				mapElementList.add(new MapElement(w, texture));
			}
		}
	}

	/**
	 * Returns the WayPoint List
	 * 
	 * @return wayPointList
	 */
	public List<WayPoint> getWayPointList() {
		return wayPointList;
	}

	/**
	 * Returns the SpawnPoint List
	 * 
	 * @return spawnPointList
	 */
	public List<SpawnPoint> getSpawnPointList() {
		return spawnPointList;
	}

	/**
	 * Returns the MapElement List
	 * 
	 * @return mapElementList
	 */
	public List<MapElement> getMapElementList() {
		return mapElementList;
	}

	/**
	 * Returns the Graphical Element List
	 * 
	 * @return mapElementList
	 */
	public List<? extends GraphicalElement> getGraphicalElementList() {
		List<GraphicalElement> graphicalElementList = new ArrayList<GraphicalElement>();
		logger.trace("MapElements: {}", getMapElementList().size() );
		graphicalElementList.addAll(getMapElementList());
		
		logger.trace("Creeps: {} of {}", getCreepList().size(), creepList.size() );
		graphicalElementList.addAll(getCreepList());
		logger.trace("Tower: {}", getTowerList().size() );
		graphicalElementList.addAll(getTowerList());
		logger.trace("Shot: {}", getShotList().size() );
		graphicalElementList.addAll(getShotList());
		return graphicalElementList;
	}

	/**
	 * Returns the Creep List
	 * 
	 * @return creepList
	 */
	public List<Creep> getCreepList() {
		// Check if the game is lost
		try {
			if (creepList.size() >= match.getPlayerCount() * maxCreep) {
				match.lose();
			}
		} catch (RemoteException e) {
			logger.error("Can't lose the Game :)", e);
		}
		return creepList;
	}

	/**
	 * Returns the Shootable List
	 * 
	 * @return creepList
	 */
	public List<? extends Shootable> getShootableList() {
		return getCreepList();
	}

	/**
	 * Spawns a creep at the current players spawn locations of the specified
	 * type. All Spawnpoints will be used to Spawn All Creeps
	 */
	public void spawnCreep(CreepPrototype creepPrototype, SpawnPoint spawnPoint) {
		logger.trace("Spawning new Creep {} at {}", creepPrototype.getId(), spawnPoint.getX() + ":" + spawnPoint.getY());
		Creep newCreep = new Creep(spawnPoint, creepPrototype);
		creepList.add(newCreep);
		newCreep.startMoving();
	}

	/**
	 * Spawns a Tower at a specified Location with a Owner
	 * 
	 * @param Point
	 * @param towerPrototype
	 * @param owner
	 * @throws RessourceException
	 * @throws TowerBuildException
	 */
	public void buildTower(Point p, TowerPrototype towerPrototype, Player owner) throws RessourceException, TowerBuildException {
		if (owner.hasRessource(towerPrototype.getCost())) {
			logger.debug("Build Tower {} at {}", towerPrototype.getId(), ConstructionZone.getGridLocation(p).getX() +":" +  ConstructionZone.getGridLocation(p).getY());
			logger.debug("point {}", p.toString());
			Dimension d = new Dimension(towerPrototype.getSize(), towerPrototype.getSize());
			logger.debug("dimension {}", d.toString());
			logger.debug("owner {}", owner.toString());
			Point gridPoint = isGridLocationValid(p, d, owner);
			
			if(gridPoint != null) {
				logger.debug("Tower built successfully");
				Tower newTower = new Tower(towerPrototype, gridPoint, owner, this);
				addTower(newTower);
			} else {
				logger.debug("Can't build Tower at this location");
				throw new TowerBuildException("Can't build here");
			}

		} else {
			throw new RessourceException("Not enough money.");
		}
	}

	/**
	 * Adds a Tower to the Towerlist
	 * 
	 * @param newTower
	 */
	private void addTower(Tower newTower) {
		towerList.add(newTower);

	}

	/**
	 * Get the Towerlist
	 * 
	 * @return towerlist
	 */
	public List<Tower> getTowerList() {
		return this.towerList;
	}

	/**
	 * Get all Shooters as List
	 * 
	 * @return
	 */
	public List<? extends Shooter> getShooterList() {
		return getTowerList();
	}

	/**
	 * Update all Movable Lists to check if an elements needs to be removed
	 */
	synchronized public void updateMovableLists() {
		updateCreepList();
		updateShotList();
	}

	/**
	 * Check if a creep is dead. If yes, remove it from the List
	 */
	private void updateCreepList() {
		List<Creep> tempCreepList = new ArrayList<Creep>(creepList);
		for (Creep c : creepList) {
			if (!c.isAlive()) {
				logger.trace("Kill: {} @ {}", c.getId(), c.getLocation());
				tempCreepList.remove(c);
			}
		}
		creepList = Collections.synchronizedList(new ArrayList<Creep>(tempCreepList));
	}

	/**
	 * Check all Shots if they have hit their Target. If yes: remove it from the
	 * List
	 */
	private void updateShotList() {
		List<Shot> tempShotList = new ArrayList<Shot>(shotList);
		for (Shot s : shotList) {
			if (s.isTargetHit()) {
				logger.trace("Hit: {} ", s.getLocation());
				tempShotList.remove(s);
			}
		}
		shotList = Collections.synchronizedList(new ArrayList<Shot>(tempShotList));
	}

	/**
	 * Add a new Shot to the Shotlist
	 * 
	 * @param shot
	 */
	public void addShot(Shot shot) {
		shotList.add(shot);
		shot.startMoving();

	}

	/**
	 * Set the Map Size
	 * 
	 * @param size
	 */
	private void setSize(int x, int y) {
		this.mapXsize = x;
		this.mapYsize = y;
	}

	/**
	 * Returns the Map Size
	 * 
	 * @return
	 */
	public Dimension getMapSize() {
		return new Dimension(getXSize(), getYSize());
	}

	/**
	 * Get X Map Size
	 * 
	 * @return int
	 */
	public int getXSize() {
		return mapXsize;
	}

	/**
	 * Get Y Map Size
	 * 
	 * @return int
	 */
	public int getYSize() {
		// Todo: Eventuell noch Y Size implementieren ?
		return mapYsize;
	}

	/**
	 * Set Name
	 * 
	 * @param name
	 */
	private void setName(String name) {
		this.name = name;
	}

	/**
	 * Get Name
	 * 
	 * @return name
	 */
	public String getName() {
		return name;
	}

	/**
	 * Get Movable List
	 * 
	 * @return MovableList
	 */
	public List<? extends Movable> getMovableList() {
		List<Movable> movableList = new ArrayList<Movable>();
		movableList.addAll(getCreepList());
		movableList.addAll(getShotList());
		return movableList;
	}

	/**
	 * Get Shot List
	 * 
	 * @return ShotList
	 */
	public List<Shot> getShotList() {
		return this.shotList;
	}

	/**
	 * Check if a GridLocation is Valid
	 * 
	 * @param point
	 * @param owner
	 * @return true/false
	 */
	public Point isGridLocationValid(Rectangle rect, Player owner) {
		Point point = ConstructionZone.getGridLocation(rect.getLocation());
		logger.trace("Grid Point: {}", point.toString());
		logger.trace("Player: {}", owner.toString());
		Rectangle testRect = new Rectangle(rect);
		testRect.setLocation(point);
		boolean isValid = false;
		for (ConstructionZone cz : owner.getMapConstructionZoneList()) {
			logger.trace("checking zone {}", cz.toString());
			if (cz.contains(testRect)) {
				logger.trace("is IN Constructionzone");
				for (Tower t : towerList) {
					logger.trace("but there is already a Tower --> False");
					if (t.getLocation().equals(point)) {
						return null;
					}
				}
				logger.trace("setting returnvalue to True");
				isValid = true;
			}
		}
		logger.trace("tower location is " + isValid);
		if(isValid) {
			return point;
		} else {
			return null;
		}
	}
	
	public Point isGridLocationValid(Point p, Dimension d, Player owner) {
		return isGridLocationValid(new Rectangle(p, d), owner);
	}

	public List<? extends GraphicalElement> getStaticMapObjects() {
		List<GraphicalElement> staticMapObjects = new ArrayList<GraphicalElement>();
		logger.trace("StaticMapObjects: {}", getMapElementList().size() );
		staticMapObjects.addAll(getMapElementList());
		return staticMapObjects;
	}

	public List<? extends GraphicalElement> getTowersMapObjects() {
		List<GraphicalElement> towersMapObjects = new ArrayList<GraphicalElement>();
		logger.trace("TowerObjects: {}", getMapElementList().size() );
		towersMapObjects.addAll(getTowerList());
		return towersMapObjects;
	}

	public List<? extends GraphicalElement> getMovableMapObjects() {
		List<GraphicalElement> movableMapObjects = new ArrayList<GraphicalElement>();
		logger.trace("CreepsObjects: {}", getMapElementList().size() );
		movableMapObjects.addAll(getCreepList());
		logger.trace("ShotObjects: {}", getMapElementList().size() );
		movableMapObjects.addAll(getShotList());
		return movableMapObjects;
	}

	public int getCreepCount() {
		return creepList.size();
	}

}
